package ch.holzi.paperracers.view;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;

import ch.holzi.paperracers.graphics.Block;
import ch.holzi.paperracers.view.Cubic;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.Bitmap.Config;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Display;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.WindowManager;
import android.widget.ImageView;

public class GameView extends SurfaceView implements SurfaceHolder.Callback//extends ImageView
{
    //These two constants specify the minimum and maximum zoom
    private static float MIN_ZOOM = 1f;
    private static float MAX_ZOOM = 5f;
    
	protected ScaleGestureDetector		mScaleDetector;
	protected GestureDetector			mGestureDetector;
	protected float					mCurrentScaleFactor;
	protected float					mScaleFactor = MAX_ZOOM / 3;
	protected int						mDoubleTapDirection;
	protected GestureListener			mGestureListener;
	protected ScaleListener			mScaleListener;
    
	private static float mDisplayWidth;
	private static float mDisplayHeight;

    //These constants specify the mode that we're in
	enum Mode
	{
		CAPTURE_TRACK, PLAY;
	}
    
    private Mode mMode;
    
	public static final String			LOG_TAG				= "Game View";
	
	protected Matrix						mBaseMatrix			= new Matrix();
	protected Matrix						mSuppMatrix			= new Matrix();
	protected Handler						mHandler			= new Handler();
	protected Runnable						mOnLayoutRunnable	= null;
	protected final Matrix					mDisplayMatrix		= new Matrix();
	protected final float[]				mMatrixValues		= new float[9];
	protected int							mThisWidth			= -1, mThisHeight = -1;
	

	private Track mTrack;
    private float mX, mY, mX0, mY0;
    private boolean mAddSegment;
    private static final float TOUCH_TOLERANCE = 4;
	private Paint 	p1 = new Paint();
	private Paint  p2 = new Paint();
	private Paint  p3 = new Paint();
		
	private Bitmap 	mBitmapDisplayed;
	
	private SurfaceHolder mSurfaceHolder;
    /** The thread that actually draws the animation */
    private GameThread mThread;
    // period of the draw() call inside the thread, in ms.
    private int mRefreshRate = 10;

    public GameView(Context context, AttributeSet attrs) {
        super(context, attrs);
        try
        {   
        	//this.setScaleType(ScaleType.MATRIX);
        	WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        	Display display = wm.getDefaultDisplay();
        	Point size = new Point();
        	display.getSize(size);
        	mDisplayWidth = size.x;
        	mDisplayHeight = size.y;
        	
        	SurfaceHolder holder = getHolder();
            holder.addCallback(this);
            mSurfaceHolder = holder;
        	
	        setFocusable(true); // make sure we get key events
	        
	        mBitmapDisplayed = Bitmap.createBitmap((int)mDisplayWidth, (int)mDisplayHeight, Config.RGB_565);

			mGestureListener = new GestureListener();
			mScaleListener = new ScaleListener();
			
			mScaleDetector = new ScaleGestureDetector( getContext(), mScaleListener );
			mGestureDetector = new GestureDetector( getContext(), mGestureListener, null, true );
			mCurrentScaleFactor = 1f;
			mDoubleTapDirection = 1;
			
			mTrack = new Track();
			
		    p1.setColor(0xffff0000);
		    p1.setStyle(Paint.Style.STROKE);
		    p2.setColor(0xff00ff00);
		    p2.setStyle(Paint.Style.STROKE);
		    p3.setColor(0xff0000ff);
		    p3.setStrokeWidth(3);
        }
        catch (Exception e)
        {
            //Log.d("GAME VIEW", "Error: " + e.getMessage());
            //Log.d("GAME VIEW", "Stack: \n" + getStackTrace(e));
        }
    }
       
    /**
     * Standard window-focus override. Notice focus lost so we can pause on
     * focus lost. e.g. user switches to take a call.
     */
    @Override
    public void onWindowFocusChanged(boolean hasWindowFocus)
    {
        if (!hasWindowFocus)
        {
        	
        }
    }
    
    public static PointF getDisplaySize()
    {
    	return new PointF(mDisplayWidth, mDisplayHeight);
    }
    
    private void touch_start(float x, float y)
    {
    	mMode = Mode.CAPTURE_TRACK;
        mTrack.reset();
        mX = x;
        mY = y;
        mX0 = x;
        mY0 = y;
        mAddSegment = true;
    }
    private void touch_move(float x, float y)
    {
    	if (mAddSegment)
    	{
    		mTrack.addSegment(new TrackSegment((x + mX)/2, (y + mY)/2, null));
    		mX = x;
    		mY = y;
    		mAddSegment = false;
    		return;
    	}
    	
        float dx = Math.abs(x - mX);
        float dy = Math.abs(y - mY);
        if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE)
        {
        	mTrack.setSegment(mTrack.getSize()-1, mX, mY, (x + mX)/2, (y + mY)/2);
        	mTrack.addSegment(new TrackSegment((x + mX)/2, (y + mY)/2, null));
            mX = x;
            mY = y;
            mAddSegment = false;
        }
    }
    
    private void touch_up(float x, float y)
    {
    	PointF lastValidPoint = mTrack.findLastValidSegment(x, y, mX0, mY0);
		if (lastValidPoint != null)
		{
			Log.d(LOG_TAG, "adding last segment");
			mTrack.addSegment(new TrackSegment(lastValidPoint.x, lastValidPoint.y));
			mTrack.setSegment(mTrack.getSize()-1, mX0, mY0, mTrack.getSegment(0).getStartPoint().x, mTrack.getSegment(0).getStartPoint().y);
		}
		else
		{
			Log.d(LOG_TAG, "filling up with segments");
	    	if (mTrack.getSize() % 2 == 0)
	    	{
	    		mTrack.setSegment(mTrack.getSize()-1, mX, mY, (x + mX)/2, (y + mY)/2);
	    		mTrack.addSegment(new TrackSegment((x + mX)/2, (y + mY)/2));
	    		mTrack.setSegment(mTrack.getSize()-1, mX0, mY0, mTrack.getSegment(0).getStartPoint().x, mTrack.getSegment(0).getStartPoint().y);
	    	}
	    	else
	    	{
	    		mTrack.setSegment(mTrack.getSize()-1, mX, mY, (mX0 + mX)/2, (mY0 + mY)/2);
	    		mTrack.addSegment(new TrackSegment((mX0 + mX)/2, (mY0 + mY)/2));
	    		mTrack.setSegment(mTrack.getSize()-1, mX0, mY0, mTrack.getSegment(0).getStartPoint().x, mTrack.getSegment(0).getStartPoint().y);
	    	}
		}
    	
    	mTrack.close();
    	mMode = Mode.PLAY;
    }
    
    
    
    @Override
    public boolean onTouchEvent(MotionEvent event) {

    	float x, y;
    	mScaleDetector.onTouchEvent( event );
		if ( !mScaleDetector.isInProgress() ) mGestureDetector.onTouchEvent( event );
		int action = event.getAction();
		switch ( action & MotionEvent.ACTION_MASK ) 
		{
			case MotionEvent.ACTION_DOWN:
				x = event.getX();
				y = event.getY();
				
				RectF image = getBitmapRect();
				float imgX = ( x - image.left ) ;
				float imgY = ( y - image.top );
				//Log.d(LOG_TAG, "x= "+x);
				//Log.d(LOG_TAG, "y= "+y);
				//Log.d(LOG_TAG, "img x= "+imgX);
				//Log.d(LOG_TAG, "img y= "+imgY);
				//Log.d(LOG_TAG, "game x = " + (int)(imgX / (12*mCurrentScaleFactor)));
				//Log.d(LOG_TAG, "game y = " + (int)(imgY / (12*mCurrentScaleFactor)));
				if ( getScale() == 1f )
				{
					x = event.getX();
					y = event.getY();
					touch_start(x, y);
					Log.d(LOG_TAG, "mode= "+mMode);
				}
				break;
			case MotionEvent.ACTION_MOVE:
				if ( getScale() == 1f )
				{
					x = event.getX();
					y = event.getY();
					touch_move(x, y);
					Log.d(LOG_TAG, "mode= "+mMode);
				}
				break;
			case MotionEvent.ACTION_UP:
				if ( getScale() == 1f )
				{
					x = event.getX();
					y = event.getY();
					touch_up(x, y);
					Log.d(LOG_TAG, "mode= "+mMode);
				}
				if ( getScale() < 1f ) {
					zoomTo( 1f, 50 );
				}
				break;
		}
		return true;
    }
    
    @Override
    public void onDraw(Canvas canvas) 
    {
    	canvas.save();
    	canvas.concat(mDisplayMatrix);
	    for (int i = 0; i < 60; i++)
	    {
	    	for (int j = 0; j < 106; j++)
	    	{
	    		Block.draw(canvas, 12 * i, 12 * j, 12);
	    	}
	    }
	    canvas.drawPoint(mX0, mY0, p3);
    	if (mMode == Mode.CAPTURE_TRACK)
    	{
    		mTrack.onDraw(canvas, Track.Mode.CREATING);
    	}
    	else if (mMode == Mode.PLAY)
    	{
    		mTrack.onDraw(canvas, Track.Mode.CREATED);
    	}
	    canvas.restore();
    }
    
	protected float onDoubleTapPost( float scale, float maxZoom )
	{
		if ( mDoubleTapDirection == 1 ) {
			if ( ( scale + ( mScaleFactor * 2 ) ) <= maxZoom ) {
				return scale + mScaleFactor;
			} else {
				mDoubleTapDirection = -1;
				return maxZoom;
			}
		} else {
			mDoubleTapDirection = 1;
			return 1f;
		}
	}
    
    
	protected float getMaxZoom()
	{
		return MAX_ZOOM;
	}
    
	/**
	 * the matrix used for displaying the canvas (after translation and scaling)
	 * @return
	 */
	protected Matrix getImageViewMatrix()
	{
		mDisplayMatrix.set( mBaseMatrix );
		mDisplayMatrix.postConcat( mSuppMatrix );
		return mDisplayMatrix;
	}
    
	/**
	 * 
	 * @param matrix
	 * @param whichValue
	 * @return the requested matrix value
	 */
	protected float getValue( Matrix matrix, int whichValue )
	{
		matrix.getValues( mMatrixValues );
		return mMatrixValues[whichValue];
	}
    
	/**
	 * @return the matrix transformed bitmap boundaries (scaled / translated)
	 */
	protected RectF getBitmapRect()
	{
		Matrix m = getImageViewMatrix();
		RectF rect = new RectF( 0, 0, mBitmapDisplayed.getWidth(), mBitmapDisplayed.getHeight() );
		m.mapRect( rect );
		return rect;
	}
    
	/**
	 * @return scaling value of the matrix
	 */
	protected float getScale()
	{
		return getValue( mSuppMatrix, Matrix.MSCALE_X );
	}
    
	/**
	 * align the displayed part of the bitmap with respect to the screen.
	 * I think mainly in case we're scaling down (--> put (0,0) of img to (0,0) of screen)
	 * @param horizontal align in horizontal direction
	 * @param vertical align in vertical direction
	 */
	protected void center( boolean horizontal, boolean vertical )
	{
		RectF rect = getCenter( horizontal, vertical );
		if ( rect.left != 0 || rect.top != 0 ) {
			postTranslate( rect.left, rect.top );
		}
	}
	
	/**
	 * calculate the center of the bitmap with respect to the screen
	 * @param horizontal get horizontal center
	 * @param vertical get vertical center
	 * @return rectangle with left/top as center coordinates
	 */
	protected RectF getCenter( boolean horizontal, boolean vertical )
	{
		RectF rect = getBitmapRect();
		float height = rect.height();
		float width = rect.width();
		float deltaX = 0, deltaY = 0;
		if ( vertical ) {
			int viewHeight = getHeight();
			if ( height < viewHeight ) {
				deltaY = ( viewHeight - height ) / 2 - rect.top;
			} else if ( rect.top > 0 ) {
				deltaY = -rect.top;
			} else if ( rect.bottom < viewHeight ) {
				deltaY = getHeight() - rect.bottom;
			}
		}
		if ( horizontal ) {
			int viewWidth = getWidth();
			if ( width < viewWidth ) {
				deltaX = ( viewWidth - width ) / 2 - rect.left;
			} else if ( rect.left > 0 ) {
				deltaX = -rect.left;
			} else if ( rect.right < viewWidth ) {
				deltaX = viewWidth - rect.right;
			}
		}
		return new RectF( deltaX, deltaY, 0, 0 );
	}
    
	/**
	 * translate the bitmap by given deltaX and deltaY
	 * @param deltaX
	 * @param deltaY
	 */
	protected void postTranslate( float deltaX, float deltaY )
	{
		mSuppMatrix.postTranslate( deltaX, deltaY );
	}
	
	/**
	 * scale the bitmap by scale factor with pivot point (centerX, centerY)
	 * @param scale
	 * @param centerX
	 * @param centerY
	 */
	protected void postScale( float scale, float centerX, float centerY )
	{
		mSuppMatrix.postScale( scale, scale, centerX, centerY );
	}
    
	/**
	 * zoom at center of screen with given scale factor for the given amount of milliseconds
	 * @param scale
	 * @param durationMs
	 */
	protected void zoomTo( float scale, float durationMs )
	{
		float cx = getWidth() / 2F;
		float cy = getHeight() / 2F;
		zoomTo( scale, cx, cy, durationMs );
	}
	
	/**
	 * zoom at given point (centerX, centerY) with given scale factor
	 * @param scale
	 * @param centerX
	 * @param centerY
	 */
	protected void zoomTo( float scale, float centerX, float centerY )
	{
		//if ( scale > MAX_ZOOM ) scale = MAX_ZOOM; // unnötig!?
		float oldScale = getScale();
		float deltaScale = scale / oldScale;
		postScale( deltaScale, centerX, centerY );
		onZoom( getScale() );
		center( true, true );
	}
	
	/**
	 * update current scale factor <-- maybe remove???
	 */
	protected void onZoom( float scale )
	{
		if ( !mScaleDetector.isInProgress() ) mCurrentScaleFactor = scale;
	}
	
	protected void scrollBy( float x, float y )
	{
		RectF rect = getBitmapRect();
		RectF srect = new RectF( x, y, 0, 0 );
		updateRect( rect, srect );
		postTranslate( srect.left, srect.top );
		center( true, true );
	}
	
	/**
	 * update bitmap boundaries when translating it
	 * @param bitmapRect
	 * @param scrollRect
	 */
	protected void updateRect( RectF bitmapRect, RectF scrollRect )
	{
		float width = getWidth();
		float height = getHeight();
		
		if ( bitmapRect.top >= 0 && bitmapRect.bottom <= height ) scrollRect.top = 0;
		if ( bitmapRect.left >= 0 && bitmapRect.right <= width ) scrollRect.left = 0;
		if ( bitmapRect.top + scrollRect.top >= 0 && bitmapRect.bottom > height ) scrollRect.top = (int)( 0 - bitmapRect.top );
		if ( bitmapRect.bottom + scrollRect.top <= ( height - 0 ) && bitmapRect.top < 0 ) scrollRect.top = (int)( ( height - 0 ) - bitmapRect.bottom );
		if ( bitmapRect.left + scrollRect.left >= 0 ) scrollRect.left = (int)( 0 - bitmapRect.left );
		if ( bitmapRect.right + scrollRect.left <= ( width - 0 ) ) scrollRect.left = (int)( ( width - 0 ) - bitmapRect.right );
		// Log.d( LOG_TAG, "scrollRect(2): " + scrollRect.toString() );
	}
    
	
	/**
	 * scroll by given x,y distance during the given amount of milliseconds
	 * @param distanceX
	 * @param distanceY
	 * @param durationMs
	 */
	protected void scrollBy( float distanceX, float distanceY, final float durationMs )
	{
		final float dx = distanceX;
		final float dy = distanceY;
		final long startTime = System.currentTimeMillis();
		mHandler.post( new Runnable() {
			
			float	old_x	= 0;
			float	old_y	= 0;
			
			public void run()
			{
				long now = System.currentTimeMillis();
				float currentMs = Math.min( durationMs, now - startTime );
				float x = Cubic.easeOut( currentMs, 0, dx, durationMs );
				float y = Cubic.easeOut( currentMs, 0, dy, durationMs );
				scrollBy( ( x - old_x ), ( y - old_y ) );
				old_x = x;
				old_y = y;
				if ( currentMs < durationMs ) {
					mHandler.post( this );
				} else {
					RectF centerRect = getCenter( true, true );
					if ( centerRect.left != 0 || centerRect.top != 0 ) scrollBy( centerRect.left, centerRect.top );
				}
			}
		} );
	}
	
	protected void zoomTo( float scale, final float centerX, final float centerY, final float durationMs )
	{
		// Log.d( LOG_TAG, "zoomTo: " + scale + ", " + centerX + ": " + centerY );
		final long startTime = System.currentTimeMillis();
		final float incrementPerMs = ( scale - getScale() ) / durationMs;
		final float oldScale = getScale();
		mHandler.post( new Runnable() {
			
			public void run()
			{
				long now = System.currentTimeMillis();
				float currentMs = Math.min( durationMs, now - startTime );
				float target = oldScale + ( incrementPerMs * currentMs );
				zoomTo( target, centerX, centerY );
				if ( currentMs < durationMs ) {
					mHandler.post( this );
				} else {
					// if ( getScale() < 1f ) {}
				}
			}
		} );
	}
	
    private String getStackTrace(Throwable t)
    {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw, true);
        t.printStackTrace(pw);
        pw.flush();
        sw.flush();
        return sw.toString();
    }

    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener 
    {
    	public ScaleListener()
    	{
    	}

        @Override
		public boolean onScale( ScaleGestureDetector detector )
		{
			float targetScale = mCurrentScaleFactor * detector.getScaleFactor();

			targetScale = Math.min( getMaxZoom(), Math.max( targetScale, MIN_ZOOM ) );
			zoomTo( targetScale, detector.getFocusX(), detector.getFocusY() );
			mCurrentScaleFactor = Math.min( getMaxZoom(), Math.max( targetScale, MIN_ZOOM ) );
			//Log.d(LOG_TAG, "scale= "+mCurrentScaleFactor);
			mDoubleTapDirection = 1;
			return true;
		}
    }
    
    private class GestureListener extends GestureDetector.SimpleOnGestureListener
    {

    	public GestureListener()
    	{

    	}
    	
		@Override
		public boolean onDoubleTap( MotionEvent e )
		{
			float scale = getScale();
			float targetScale = scale;
			targetScale = onDoubleTapPost( scale, getMaxZoom() );
			targetScale = Math.min( getMaxZoom(), Math.max( targetScale, MIN_ZOOM ) );
			mCurrentScaleFactor = targetScale;
			zoomTo( targetScale, e.getX(), e.getY(), 200 );
			return super.onDoubleTap( e );
		}
		
		
		@Override
		public boolean onScroll( MotionEvent e1, MotionEvent e2, float distanceX, float distanceY )
		{
			if ( e1 == null || e2 == null ) return false;
			if ( e1.getPointerCount() > 1 || e2.getPointerCount() > 1 ) return false;
			if ( mScaleDetector.isInProgress() ) return false;
			if ( getScale() == 1f ) return false;
			scrollBy( -distanceX, -distanceY );
			invalidate();
			return super.onScroll( e1, e2, distanceX, distanceY );
		}
		
		@Override
		public boolean onFling( MotionEvent e1, MotionEvent e2, float velocityX, float velocityY )
		{
			if ( e1.getPointerCount() > 1 || e2.getPointerCount() > 1 ) return false;
			if ( mScaleDetector.isInProgress() ) return false;
			
			float diffX = e2.getX() - e1.getX();
			float diffY = e2.getY() - e1.getY();
			
			if ( Math.abs( velocityX ) > 800 || Math.abs( velocityY ) > 800 ) {
				scrollBy( diffX / 2, diffY / 2, 300 );
				invalidate();
			}
			return super.onFling( e1, e2, velocityX, velocityY );
		}
    }

	@Override
	public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void surfaceCreated(SurfaceHolder arg0) 
	{
        // start the thread here so that we don't busy-wait in run()
        // waiting for the surface to be created
        try
        {
            mThread.setRunning(true);
            mThread.start();
            Log.d(LOG_TAG, "surfaceCreated() called");
        }
        catch (Exception e)
        {
            Log.d(LOG_TAG, "Error: " + e.getMessage());
            Log.d(LOG_TAG, "Stack: \n" + getStackTrace(e));
        }
	}

	@Override
	public void surfaceDestroyed(SurfaceHolder arg0) 
	{
		synchronized(mSurfaceHolder)
		{
			mThread.setRunning(false);
			Log.d(LOG_TAG, "surfaceDestroyed() called");
		}
	}
	
    public GameThread getThread()
    {
        return mThread;
    }
    
    public GameThread createThread()
    {
        mThread = new GameThread(this);
        return mThread;
    }

    public void destroyThread()
    {
        try
        {
            mThread.join();
            mThread = null;
        }
        catch (Exception e)
        {
            Log.d("GAME VIEW", "Error: " + e.getMessage());
            Log.d("GAME VIEW", "Stack: \n" + getStackTrace(e));
        }
    }
	
	
    public class GameThread extends Thread
    {
        /*
         * State-tracking constants
         */
        public static final int GAME_PAUSED = 1;
        public static final int GAME_RUNNING = 2;
    	
        /** Indicate whether the surface has been created & is ready to draw */
        private boolean mRun = false;
        
        /** The state of the thread. One RUNNING, PAUSED */
        private int mMode;
        
        private GameView mView;
        
        public GameThread(GameView view)
        {
            mMode = GAME_PAUSED;
            mView = view;
        }
        
        @Override
        public void run()
        {
            while (mRun)
            {
                Canvas c = null;
                try
                {
                    c = mSurfaceHolder.lockCanvas(null);
                    synchronized (mSurfaceHolder)
                    {
                    	if (mMode == GAME_RUNNING)
                        {
                    		mView.onDraw(c);
                        }
                    }
                    sleep(mRefreshRate);
                }
                catch (Exception e)
                {
                    Log.d("GAME VIEW", "Error: " + e.getMessage());
                    Log.d("GAME VIEW", "Stack: \n" + getStackTrace(e));
                }
                finally
                {
                    // do this in a finally so that if an exception is thrown
                    // during the above, we don't leave the Surface in an
                    // inconsistent state
                    if (c != null)
                    {
                        mSurfaceHolder.unlockCanvasAndPost(c);
                    }
                }
            }
        }
        
        /**
         * Used to signal the thread whether it should be running or not.
         * Passing true allows the thread to run; passing false will shut it
         * down if it's already running. Calling start() after this was most
         * recently called with false will result in an immediate shutdown.
         * 
         * @param b
         *            true to run, false to shut down
         */
        public void setRunning(boolean b)
        {
            mRun = b;
        }
        
        public void unpause()
        {
            synchronized (mSurfaceHolder)
            {
                if (mMode == GAME_PAUSED)
                    setState(GAME_RUNNING);
            }
        }

        /**
         * Pauses the animation.
         */
        public void pause()
        {
            synchronized (mSurfaceHolder)
            {
                if (mMode == GAME_RUNNING)
                    setState(GAME_PAUSED);
            }
        }
        
        /**
         * Sets the game mode. That is, whether we are running, paused, in the
         * failure state, in the victory state, etc.
         * 
         * @see #setState(int, CharSequence)
         * @param mode
         *            one of the STATE_* constants
         */
        public void setState(int mode)
        {
            synchronized (mSurfaceHolder)
            {
                mMode = mode;
            }
        }
        
        private String getStackTrace(Throwable t)
        {
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw, true);
            t.printStackTrace(pw);
            pw.flush();
            sw.flush();
            return sw.toString();
        }
        
    }
}

